package edu.hawaii.ics414.blackjack.player;

import edu.hawaii.ics414.blackjack.card.Card;
import edu.hawaii.ics414.blackjack.card.GameDeck;
import edu.hawaii.ics414.blackjack.card.Rank;
import java.util.Arrays;

/**
 * An individual spot at a Black Jack table.
 * The class name "Player" would have been preferred, but that seems to better fit the human user.
 * 
 * @author Branden Ogata
 *
 */

public abstract class Seat
{
  /**
   * The hand(s) of the player at this Seat.
   * 
   */
  public Card[][] hands;
  
  /**
   * Indicates if the player has chosen to stand for a particular hand.
   * 
   */
  protected boolean[] stands;
  
  /**
   * The bet(s) of the player at this Seat.
   * 
   */
  protected int[] bets;
  
  /**
   * The total money available to the player.
   * 
   */
  protected int money;
  
  /**
   * Indicates whether this player won, lost, or tied the last round.
   * Equal to 1 if this player won, -1 if this player lost, and 0 if this player tied.
   * 
   */
  protected int lastRound;
  
  /**
   * Stores the bets from the previous round.
   * 
   */
  protected int[] lastBet;
  
  public boolean hand1DidnotSplit = true;
  public boolean hand2DidnotSplit = true;
  public boolean hand3DidnotSplit = true;
  public boolean hand4DidnotSplit = false;
  
  /**
   * Returns the money of this player.
   *
   * @return money The int containing the money of this player.
   *
   */
  
  public int getMoney()
  {
    return this.money;
  }

  /**
   * Sets the money of this player.
   *
   * @param money The int containing the money to set for this player.
   *
   */
  
  public void setMoney(int money)
  {
    this.money = money;
  }
  
  /**
   * Provides this player with a new hand.
   * 
   * @param deck The GameDeck to take cards from.
   * 
   */
  
  public void startHand(GameDeck deck)
  {
    // If first hand is empty
    if (this.countNumberOfCards(0) == 0)
    {
      this.hands[0][0] = deck.draw();
      this.hands[0][1] = deck.draw();
    }
    else
    {
      throw new IllegalStateException("Cannot start a new hand.");
    }
  }

  /**
   * Draws one card from the deck to add to the designated hand.
   * 
   * @param deck The GameDeck from which to get cards.
   * @param hand The int indicating the hand to hit to.
   * 
   */
  
  protected void hit(GameDeck deck, int hand)
  {
    // If hand is valid
    if ((this.isValidHand(hand)) && (!this.stands[hand]))
    {
      Card top = deck.draw();
      int index = 0;
      
      // Find an available place to put the card in the hand indicated
      while ((index < hands[hand].length) && (hands[hand][index] != null))
      {
        index++;
      }
      
      // If a valid spot was found, then add the card
      if (index < hands[hand].length)
      {
        hands[hand][index] = top;
      }
      else
      {
        throw new IllegalArgumentException("A hand can only have five cards.");
      }
    }
    else if (this.stands[hand])
    {
      throw new IllegalArgumentException("This player has already chosen to stand for this hand.");
    }
    else
    {
      throw new IllegalArgumentException("Hand must be between 0 and 3.");      
    }
  }
  
  /**
   * Indicates if it would be valid to hit.
   * 
   * @param hand The int indicating the hand to examine.
   *
   * @return valid A boolean that is true if the player can hit,
   *                                 false otherwise.
   *                                 
   */
  
  public boolean canHit(int hand)
  {
    boolean valid = true;
    
    // Can hit unless the hand is invalid, the sum is greater than 21,
    // or there are already five cards in the hand
    if ((!this.isValidHand(hand)) || (this.stands[hand]) || (this.sum(hand) >= 21) || 
        (this.countNumberOfCards(hand) == 5))
    {
      valid = false;
    }
    
    return valid;
  }
  
  /**
   * Decides to stop taking cards for a particular hand.
   * 
   * @param hand The int indicating the hand to stand on.
   * 
   */
  
  protected void stand(int hand)
  {
    // If hand is valid
    if ((this.isValidHand(hand)) && (!this.stands[hand]))
    {
      this.stands[hand] = true;
    }
    else if (this.stands[hand])
    {
      throw new IllegalArgumentException("This player has already chosen to stand for this hand.");
    }
    else
    {
      throw new IllegalArgumentException("Hand must be between 0 and 3.");      
    }
  }
  
  /**
   * Indicates if it would be valid to stand.
   * 
   * @param hand The int indicating the hand to examine.
   * 
   * @return valid A boolean that is true if the player can stand,
   *                                 false otherwise.
   * 
   */
  
  public boolean canStand(int hand)
  {
    boolean valid = true;
    
    // Can stand unless the hand is invalid or the player has already decided to stand
    if ((!this.isValidHand(hand)) || (this.stands[hand]))
    {
      valid = false;
    }
    
    return valid;
  }
  
  /**
   * Splits the given hand into two separate hands, adding one card to each of the new hands.
   * A split is only allowed if the hand contains two cards of equal rank.
   * This is only legal on the first opportunity; another limitation is that only three splits 
   * are allowed (thus four hands total), so splitting on this.hands[3] is not legal.
   * 
   * @param deck The GameDeck from which to get cards.
   * @param hand The int indicating the hand to split.
   * 
   */
  
  protected void split(GameDeck deck, int hand)
  {
    // If hand is valid
    if ((this.isValidHand(hand)) && (!this.stands[hand]))
    {
      // Hand must have two equal cards (the check for standing is handled in the condition above),
      // and the hand cannot be the fourth (after three splits)
      if ((this.countNumberOfCards(hand) == 2) && 
          (this.hands[hand][0].getValue() == this.hands[hand][1].getValue()) && (hand != 3))
      {
        // Move the second card from the parameter hand to the next hand
        this.hands[hand + 1][0] = this.hands[hand][1];
        
        // Add another card to both of the hands
        this.hands[hand][1] = deck.draw();
        this.hands[hand + 1][1] = deck.draw();
        
        // Set the bet for the new hand
        this.bets[hand + 1] = this.bets[hand];
      }
      else
      {
        throw new IllegalArgumentException("Cannot split the given hand.");
      }
    }
    else if (this.stands[hand])
    {
      throw new IllegalArgumentException("This player has already chosen to stand for this hand.");
    }
    else
    {
      throw new IllegalArgumentException("Hand must be between 0 and 3.");      
    }    
  }
  
  /**
   * Indicates if it would be valid to split.
   * 
   * @param hand The int indicating the hand to examine.
   * 
   * @return valid A boolean that is true if the player can split,
   *                                 false otherwise.
   * 
   */
  
  public boolean canSplit(int hand)
  {
    boolean valid = true;
    switch(hand)
    {
    case 0:
      valid = hand1DidnotSplit;
      break;
    case 1:
      valid = hand2DidnotSplit;
      break;
    case 2:
      valid = hand3DidnotSplit;
      break;
    case 3:
      valid = hand4DidnotSplit;
      break;
    }
    
    // Can split unless the hand is invalid, the number of cards is not two,
    // the values of the cards are unequal, or it is the last hand
    if ((!this.isValidHand(hand)) || (this.stands[hand]) || (this.countNumberOfCards(hand) != 2) || 
        (!(this.hands[hand][0].getRank() == this.hands[hand][1].getRank())) || (hand == 3))
    {
      valid = false;
    }
    
    return valid;
  }
  
  /**
   * Doubles the bet, adds one more card, and then stands on the given hand.
   * Doubling down is only allowed upon first receiving a hand. 
   * 
   * @param deck The GameDeck to draw a card from.
   * @param hand The int indicating the hand to double down on.
   * 
   */
  
  protected void doubleDown(GameDeck deck, int hand)
  {
    // If hand is valid
    if ((this.isValidHand(hand)) && (!this.stands[hand]))
    {
      // If this is the first time that the player has interacted with the hand
      // (that is, if there are two cards and the player did not stand previously;
      // standing was accounted for in the condition above, so only need to check 
      // the number of cards here)
      if (this.countNumberOfCards(hand) == 2)
      {
        // Double the bet
        this.bets[hand] *= 2;
        
        // Add one card to the hand
        this.hands[hand][2] = deck.draw();
        
        // Stand
        this.stands[hand] = true;
      }
    }
    else if (this.stands[hand])
    {
      throw new IllegalArgumentException("This player has already chosen to stand for this hand.");
    }
    else
    {
      throw new IllegalArgumentException("Hand must be between 0 and 3.");      
    }
  }
  
  /**
   * Indicates if it would be valid to double down.
   * 
   * @param hand The int indicating the hand to examine.
   * 
   * @return valid A boolean that is true if the player can double down,
   *                                 false otherwise.
   * 
   */
  
  public boolean canDoubleDown(int hand)
  {
    boolean valid = true;
    
    // Can double down unless the hand is invalid or the player did not do so immediately
    if ((!this.isValidHand(hand)) || (this.stands[hand]) || (this.countNumberOfCards(hand) != 2))
    {
      valid = false;
    }
    
    return valid;
  }

  /**
   * Performs the parameter action.
   * 
   * @param deck The GameDeck used in the game.
   * @param hand The int indicating the hand to use.
   * @param action The Action to perform.
   *
   * @return success A boolean that is true if the action succeeded,
   *                                   false otherwise.
   *                                   
   */
  
  public boolean play(GameDeck deck, int hand, Action action)
  {
    boolean success = false;
    
    try
    {
      switch (action)
      {
        case HIT:
          this.hit(deck, hand);
          break;
        case STAND:
          this.stand(hand);
          break;
        case SPLIT:
          this.split(deck, hand);
          break;
        case DOUBLE_DOWN:
          this.doubleDown(deck, hand);
          break;
        default: 
          break;
      }
      
      success = true;
    }
    catch (IllegalArgumentException e)
    {
      System.out.println(e.getMessage());
    }
    
    return success;
  }
  
  /**
   * Gets the bet for the indicated hand.
   * 
   * @param hand The int indicating the hand to change the bet for.
   * 
   * @return The bet for the hand indicated in the parameter.
   * 
   */
  
  public int getBet(int hand)
  {
    // If hand is valid
    if (this.isValidHand(hand))
    {
      return this.bets[hand];
    }
    else
    {
      throw new IllegalArgumentException("Hand must be between 0 and 3.");      
    }
  }
  
  /**
   * Sets the bet for the indicated hand.
   * 
   * @param hand The int indicating the hand to change the bet for.
   * @param bet The int containing the new value for the bet.
   * 
   */
  
  public void setBet(int hand, int bet)
  {
    // If hand is valid
    if (this.isValidHand(hand))
    {
      // If it is valid to change the bet for the given hand and the bet itself is valid
      if (/*(this.countNumberOfCards(hand) == 0) && */
          (this.money >= bet))
      {
        this.bets[hand] = bet;
        this.money -= this.bets[hand];
      
      }
      else if (this.countNumberOfCards(hand) != 0)
      {
        throw new IllegalArgumentException("It is too late to change the bet for this hand.");
      }
      else
      {
        throw new IllegalArgumentException("Not enough money for this bet.");
      }
    }
    else
    {
      throw new IllegalArgumentException("Hand must be between 0 and 3.");      
    }    
  }
  
  /**
   * Adds the bet from a round to the money of the player.
   *
   * @param hand The int indicating the hand that this player won.
   * 
   */
  
  public void wonRound(int hand)
  {
    // If hand is valid
    if (isValidHand(hand))
    {
      this.money += 2 * this.bets[hand];
      this.lastRound = 1;
      this.lastBet[hand] = this.bets[hand];
      this.bets[hand] = 0;
    }
    else
    {
      throw new IllegalArgumentException("Hand must be between 0 and 3.");      
    }
  }
  
  /**
   * Clears the bet on a hand from the money of the player.
   * 
   * @param hand The int indicating the hand that this player lost.
   * 
   */
  
  public void lostRound(int hand)
  {
    // If hand is valid
    if (isValidHand(hand))
    {
      this.lastRound = -1;
      this.lastBet[hand] = this.bets[hand];
      this.bets[hand] = 0;
      
    }
    else
    {
      throw new IllegalArgumentException("Hand must be between 0 and 3.");      
    }
  }
  
  /**
   * Returns the money from a bet to the player.
   * 
   * @param hand The int indicating the hand that this player tied on.
   * 
   */
  
  public void tiedRound(int hand)
  {
    // If hand is valid
    if (isValidHand(hand))
    {
      this.money += this.bets[hand];
      this.lastRound = 0;
      this.lastBet[hand] = this.bets[hand];
      this.bets[hand] = 0;
    }
    else
    {
      throw new IllegalArgumentException("Hand must be between 0 and 3.");      
    }    
  }
  
  /**
   * Returns a hand from this player.
   * 
   * @param hand The int indicating the hand that this player has.
   *             
   * @return The Card[] cards containing the hand for this player.
   * 
   * @throws InvalidArgumentException if the input is not valid.
   * 
   */
  
  protected Card[] getHand(int hand)
  {
    if (isValidHand(hand))
    {
      return this.hands[hand];
    }
    else
    {
      throw new IllegalArgumentException("Hand must be between 0 and 3.");      
    }        
  }
  
  /**
   * Returns the sum of the bets for all hands that this player has.
   * 
   * @return An int containing the sum of all bets for this player.
   * 
   */
  
  @SuppressWarnings("unused")
  private int sumBets()
  {
    // Though possibly more efficient to perform boolean check before adding,
    // this at least will not cause any problems since the values are valid
    return this.bets[0] + this.bets[1] + this.bets[2] + this.bets[3];
  }
  
  /**
   * Indicates if it possible for this player to bet a given amount.
   * 
   * @param amount The int containing the size of the bet to check.
   * 
   * @return A boolean that is true if this player has enough money for the bet,
   *                           false otherwise.
   *                           
   */
  
  public boolean canBet(int amount)
  {
    return money >= amount;
  }
  
  /**
   * Clears all hands for this player.
   * Used at the end of a round.
   * 
   */
  
  public void clear()
  {
    for (Card[] hand : this.hands)
    {
      Arrays.fill(hand, null);
    }
    Arrays.fill(this.stands, false);
    Arrays.fill(this.bets, Math.min(this.bets[0], this.money));
  }
  
  /**
   * Returns a String representation of the hands for the player.
   * 
   * @return A String representing the cards that this player has.
   * 
   */
  
  public abstract String getCards();
  
  /**
   * Checks if a given integer is a valid index for this.hands.
   * 
   * @param hand The int indicating the hand to examine.
   * 
   * @return valid A boolean that is true if hand is between 0 and 3 (inclusive);
   *                                   false otherwise.
   *                                   
   */
  
  public boolean isValidHand(int hand)
  {
    return ((0 <= hand) && (hand < 4));
  }
  
  /**
   * Counts the number of cards in the given hand.
   * 
   * @param hand The int indicating the hand to examine.
   * 
   * @return cards An int equal to the number of cards in the given hand.
   * 
   */
  
  protected int countNumberOfCards(int hand)
  {
    int cards = 0;
    
    // If hand is valid
    if (this.isValidHand(hand))
    {
      // If a card in the hand is not null, then increment cards
      for (Card c : this.hands[hand])
      {
        if (c != null)
        {
          cards++;
        }
      }
    }
    else if (this.stands[hand])
    {
      throw new IllegalArgumentException("This player has already chosen to stand for this hand.");
    }
    else
    {
      throw new IllegalArgumentException("Hand must be between 0 and 3.");      
    }        
    
    return cards;
  }
  
  /**
   * Calculates the sum of the cards in a hand.
   * 
   * @param hand The int indicating the hand to examine.
   * 
   * @return sum An int equal to the sum of the cards in the parameter hand.
   * 
   */
  
  public int sum(int hand)
  {
    int sum = 0;
    boolean hasAce = false;
    
    if (this.isValidHand(hand))
    {
      // For each Card in the hand
      for (Card c : this.hands[hand])
      {
        if (c != null)
        {
          sum += c.getValue();
          
          if (c.getRank().equals(Rank.ACE))
          {
            hasAce = true;
          }
        }
      }
    }
    
    // If the hand had an Ace and it can count as 11 without going over 21, then do so
    if ((hasAce) && (sum <= 11))
    {
      sum += 10;
    }
    
    return sum;
  }
  
  /**
   * Indicates if the given hand can be played as a soft hand.
   * "Soft" means that there is an Ace that is being played as an 11.
   * 
   * @param hand The int indicating the hand to examine.
   * 
   * @return soft A boolean that is true if the hand can be soft, 
   *                                false otherwise.
   *                                
   */
  
  protected boolean isHandSoft(int hand)
  {
    boolean soft = false;
    
    // If the hand is valid
    if (isValidHand(hand))
    {
      // Find the sum; the sum method cannot be used since it gives no indication of whether
      // an Ace was counted as high or low
      int sum = 0;
      boolean hasAce = false;
      
      for (Card c : this.hands[hand])
      {
        if (c != null)
        {
          sum += c.getValue();
          
          if (c.getRank().equals(Rank.ACE))
          {
            hasAce = true;
          }
        }
      }
      
      // If there is an Ace and it would be safe to count it as 11, then the hand is soft
      if ((hasAce) && (sum < 11))
      {
        soft = true;
      }
    }
    else if (this.stands[hand])
    {
      throw new IllegalArgumentException("This player has already chosen to stand for this hand.");
    }
    else
    {
      throw new IllegalArgumentException("Hand must be between 0 and 3.");      
    }        
    
    return soft;
  }
  
  /**
   * Indicates if the given hand has an Ace.
   * 
   * @param hand The int indicating the hand to examine.
   * 
   * @return hasAce A boolean that is true if the hand has an Ace,
   *                                  false otherwise.
   *                                  
   */
  
  protected boolean hasAce(int hand)
  {
    boolean hasAce = false;
    
    if (isValidHand(hand))
    {
      for (Card c : this.hands[hand])
      {
        if ((c != null) && (c.getRank().equals(Rank.ACE)))
        {
          hasAce = true;
        }
      }
    }
    else
    {
      throw new IllegalArgumentException("Hand must be between 0 and 3.");
    }
    
    return hasAce;
  }
    
  /**
   * Returns a String representation of the sum of the cards in all hands.
   * 
   * @return sum A String containing the sum of the cards in all hands for the player.  
   *             If Aces are involved, then provides the count with both low and high Aces;
   *             however, if a high Ace would put the sum over 21, then it is omitted.
   *             
   */
  
  @Override
  public String toString()
  {
    String sum = "";
    
    // For each hand
    for (Card[] hand : this.hands)
    {
      int total = 0;
      int totalHigh = -1;
      
      // If there are cards in the hand
      if (hand[0] != null)
      {
        // Go through each card in the hand
        for (Card c : hand)
        {
          if (c != null)
          {
            total += c.getValue();
            
            // If this is the first Ace in the hand
            if (c.getRank().equals(Rank.ACE))
            {
              // totalHigh is not changed until reaching the first Ace; 
              // only the first Ace should be counted high (two high Aces would sum to 22
              // and thus would not be valid); thus only the first should add 11 to the total
              totalHigh = (totalHigh == -1) ? (total + 10) : (totalHigh + c.getValue());
            }
          }
        }
        
        // Concatenate the sum or sums onto the hand
        sum += (((totalHigh > total) && (totalHigh <= 21)) ? (totalHigh) : (total)) + 
               (((totalHigh > total) && (totalHigh <= 21)) ? ("/" + total) : ("")) + "\n";
      }
    }
    
    return sum;
  }  
  
  /**
   * Returns a description of the indicated hand.
   * 
   * @param hand The int indicating the hand to examine.
   * 
   * @return str A String containing a description of the parameter hand.
   * 
   */
  
  public String handValue(int hand)
  {
    boolean cannotBeBlackjack = false;
    int sum = 0;
    int sumAfterAce = 0;
    boolean hasAce = false;
    String str = "";
    
    if (this.isValidHand(hand))
    {
      // For each Card in the hand
      for (Card c : this.hands[hand])
      {
        if (c != null)
        {
          sum += c.getValue();
          
          if (c.getRank().equals(Rank.ACE))
          {
            hasAce = true;
          }
        }
      }
    }
    
    str += sum;
    
    // If the hand had an Ace and it can count as 11 without going over 21, then do so
    if ((hasAce) && (sum <= 11))
    {
      sumAfterAce = sum + 10;
      str += " or ";
      str += sumAfterAce;
    }
    
    if (hasAce && sumAfterAce == 21 && !cannotBeBlackjack && 
       hands[hand][2] == null && hands[1][0] == null) 
    {
      str = "BLACKJACK";
      cannotBeBlackjack = true;
    }
    
    if (sum > 21 || sumAfterAce > 21) 
    {
      str = "BUSTED";
    }
    
    return str;
  }  
}
